package com.instant.searcher;

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;

//通过这个类完成所有的搜索过程
public class DocSearcher {
    private static final String STOP_WORD_PATH = "/root/java/project/data/stop_word.txt";
    //使用这个hashSet保存停用词
    private HashSet<String> stopWords = new HashSet<>();
    //此处要实例化索引对象
    //同时完成索引加载的工作
    private Index index = new Index();
    public DocSearcher() {
        index.load();
        loadStopWord();
    }
    //完成整个搜索的方法
    //参数（输入部分）就是用户给出的查询词
    //返回值（输出部分）就是搜索结果的集合
    public List<Result> search(String query) {
        //针对分词结果对暂停词进行过滤
        List<Term> oldTerms = ToAnalysis.parse(query).getTerms();
        List<Term> terms = new ArrayList<>();
        for (Term term : oldTerms) {
            if(stopWords.contains(term.getName())) {
               continue;
            }
            terms.add(term);
            System.out.println(term.getName());
        }


        //1.[分词]针对query这个查询词进行分词
        //List<Term> terms = ToAnalysis.parse(query).getTerms();

        //将每一个分词结果分别进行查询，查询结果综合到一个数组当中去
        //List<Weight> allTermResult = new ArrayList<>();
        List<List<Weight>> termResults = new ArrayList<>();
        //2.[触发]针对分词结果查倒排
        for (Term term : terms) {
            String word = term.getName();
            //虽然倒排索引中有很多的词，但是这里的词一定是之前文档中存在的，如果不存在就说明文档中没有，就返回空
            List<Weight> invertedList = index.getInverted(word);
            if (invertedList == null) {
                //说明所有文档中都不存在
                continue;
            }
            termResults.add(invertedList);
            //allTermResult.addAll(invertedList);
        }

        //3.合并，针对二维数组实现多路并归（按DocId顺序对文档进行排序），合并DocId相同的文档，
        List<Weight> allTermResult = merger(termResults);


        //3.[排序]针对触发的结果按照权重排序
        allTermResult.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                //需要进行降序排序
                //升序：o1 - o2
                //降序：o2 - o1   后面如果忘记就随便写，看结果对不对。
                return o2.getWeight() - o1.getWeight();
            }
        });
        //4.[包装结果]针对排序后的结果去查正排返回想要的数据
        List<Result> results = new ArrayList<>();
        for (Weight weight : allTermResult) {
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            result.setDesc(GenDesc(docInfo.getContent(), terms));
            results.add(result);
        }
        return results;
    }

    //内部类，用于表示元素位置
    static class Pos{
        public int row;
        public int col;
        public Pos(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }

    private List<Weight> merger(List<List<Weight>> source) {
        //合并的时候把多个行合成一行，合并过程中需要操做其中的每一个元素，也就是需要通过行列来描述一个元素的位置
        //1.先针对每一行按照id进行升序排序
        for(List<Weight> curRow : source){
            curRow.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId() - o2.getDocId();
                }
            });
        }
        //2.1借助一个优先队列针对这些行进行合并。
        //target 表示最终合并的结果
        List<Weight> target = new ArrayList<>();
        //创建一个优先级队列并指定比较规则
        PriorityQueue<Pos> queue= new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                //刚才已经比较出每行最小的对象，按升序排序就在数组第一个
                //此时我们需要用Pos定位到每行的第一个元素互相进行比较，此时就可以得到最小的元素
                //然后最小元素入队列，此元素所在行指针向后移动一个元素进行比较，循环此过程，得到一个按DocId升序的队列。
                Weight w1 = source.get(o1.row).get(o1.col);
                Weight w2 = source.get(o2.row).get(o2.col);
                return w1.getDocId()-w2.getDocId();
            }
        });
        //2.2对数组的头元素放置于优先级队列当中进行比较，取出最小元素，整合到数组中
        for(int row = 0;row < source.size();row++){
            queue.offer(new Pos(row , 0));
        }
        //2.3循环取队首元素
        while (!queue.isEmpty()){
            Pos minPos = queue.poll();
            Weight curWeight = source.get(minPos.row).get(minPos.col);
            //2.4看看取到的Weight中的DocId和上一个是否相同
            //相同就合并
            if(!target.isEmpty()){
                Weight lastWeight = target.getLast();
                if(lastWeight.getDocId() == curWeight.getDocId()){
                    //合并Weight
                    lastWeight.setWeight(lastWeight.getWeight() + curWeight.getWeight());
                }else{
                    target.add(curWeight);
                }
            }else{
                target.add(curWeight);
            }
            //要把对应光标向后移动来取下一个元素
            Pos newPos = new Pos(minPos.row, minPos.col + 1);
            if(newPos.col >= source.get(newPos.row).size()){
                //说明光标指向已经超出这一行的末尾,这一行处理完毕，把剩下的行以及队列中剩余元素比较完就可以
                continue;
            }
                queue.offer(newPos);
        }
        return target;
    }

    private String GenDesc(String content, List<Term> terms) {
        int firstPos = -1;
        //1.先遍历分词结果，看看哪个结果在分词中存在
        for(Term term : terms){
            //分词直接针对词进行转小写
            String word = term.getName();
            //此时需要把content也转化为小写再进行查询。
            //左右带空格说明是一个独立的词语
            //此处全字匹配更加严谨的话需要使用正则表达式。（文章头尾不能识别）
            //" " + word + " "不是很严谨，如果左边右边有括号或者其他符号，那么此word也算一个单词，但是识别不出来
            //要解决上述问题还是需要正则表达式 , \b匹配单词与空格，单词与标点符号的单词边界,indexOf不支持正则匹配
            //replace/replaceAll都是支持正则，此时我们就可以用replaceAll将左右带有标点符号的单词转化为左右带空格的单词
            content.toLowerCase().replaceAll("\\b"+word+"\\b"," "+word+" ");
            firstPos = content.toLowerCase().indexOf(" " + word + " ");

            if(firstPos >= 0) {
                //找到位置
                break;
            }
        }
            if(firstPos == -1){
                //所有分词都不在正文中存在。概率极小,此时返回一个空字符串
                return "";
                //if(content.length() > 100){
                    //return content.subString(0,100) + "...";
                //}
                //return content;
                //
            }
            String desc = "";
            int descBeg = firstPos < 60 ? 0 : firstPos - 60;
            if(descBeg + 160 < content.length()){
                desc = content.substring(descBeg, descBeg + 160);
            }else{
                desc = content.substring(descBeg) + "...";
            }
            //在此处加上替换操作，把描述中和分词结果相同的部分加上<i>标签，可以通过replace方式进行替换
        for(Term term : terms){
            String word = term.getName();
            desc = desc.replaceAll("(?i) "+word+" ", "<i> "+word+" </i>");
        }
            return desc;
    }

    public void loadStopWord() {
        try(BufferedReader bf = new BufferedReader(new FileReader(STOP_WORD_PATH))){
            while(true){
                String line = bf.readLine();
                if(line == null){
                    break;
                }
                stopWords.add(line);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        DocSearcher searcher = new DocSearcher();
        Scanner sc = new Scanner(System.in);
        while(true){
            System.out.println("请输入要查询的词语->");
            String query = sc.next();
            List<Result> results = searcher.search(query);
            for(Result result : results){
                System.out.println("===========================");
                System.out.println(result);
            }
        }
    }
}
